import java.nio.ByteBuffer;
import java.util.Arrays;



public class IPv4Datagram {

	/* A byte's range is -128 to 127, so an integer value needs to be offset
	 * by 128 so that it occupies the correct "step" of the byte.
	 * Ex: (69 of type int) minus 128 equals (-59 of type byte).
	 *      69 - 128 = -59
	 */
	private final int BYTE_OFFSET = 128;
	public static final int INITIALIZE_TTL = 255;   // Maximum time-to-live value.
	private final short SET_DO_NOT_FRAGMENT = 0x4000;  // Bitmask for do-not-fragment bit.
	private final short SET_MORE_BITS = 0x2000;        // Bitmask for set-more-bits bit.
	private byte[] datagram;

	public IPv4Datagram(short id, boolean doNotFragment, boolean moreBits,
			short fragmentOffset, int lifetime, boolean decrementTTL,
			MacAddress sourceIp, MacAddress destinationIp, byte[] payload) throws InstantiationException {

		if (lifetime < 0) {
			throw new InstantiationException("IPv4 Datagram not built: Time-to-live expired.");
		} else if (decrementTTL) {
			lifetime--;

			byte timeToLive = (byte)(lifetime - BYTE_OFFSET);

			datagram = assemble(id, doNotFragment, moreBits, fragmentOffset, timeToLive,
					sourceIp, destinationIp, payload);
		} else {
			byte timeToLive = (byte)(lifetime - BYTE_OFFSET);

			datagram = assemble(id, doNotFragment, moreBits, fragmentOffset, timeToLive,
					sourceIp, destinationIp, payload);
		}
	}

	/**
	 *  Assembles an IPv4 datagram. Maximum length is 65535 bytes.
	 *  
	 * @param id
	 * @param doNotFragment
	 * @param moreBits
	 * @param fragmentOffset
	 * @param timeToLive
	 * @param sourceIp
	 * @param destinationIp
	 * @param payload
	 * @return
	 */
	private byte[] assemble(short id, boolean doNotFragment, boolean moreBits,
			short fragmentOffset, short timeToLive, MacAddress sourceIp,
			MacAddress destinationIp, byte[] payload) {

		// The maximum length of an IPv4 datagram is 65535 bytes.
		ByteBuffer buffer = ByteBuffer.allocate(65535);

		/* Append version and header length (HLEN).
		 * They are each 4 bits in length so they'll need to be
		 * combined to make up 1 byte.
		 * IPv4 is the first half of the byte.
		 * HLEN is the second half of the byte.
		 * IPv4 is 4 hex. HLEN is 5 hex. 45 hex equals 69 decimal.
		 */
		buffer.put((byte)(69 - BYTE_OFFSET));

		/* Append service type, 8 bits. Value = 0 as specified in the
		 * project requirements.
		 */
		buffer.put((byte)(0 - BYTE_OFFSET));

		/* Bytes 2 and 3 are reserved for the total length of the
		 * IPv4 datagram. Bytes are being added via 'relative put',
		 * so just use a filler value to skip them for now.
		 */
		buffer.putShort((short)555);   // Filler value for bytes 2 and 3.

		// Append identification, 16 bits.buffer.
		buffer.putShort(id);

		/* Append fragment flags and offsets.
		 * Flags = 3 bits.
		 * Fragment offset = 13 bits.
		 * Combine the "fields" into a single 2-byte value.
		 */
		int fragmentData = fragmentOffset;

		// Bitwise operation to set the appropriate bit.
		if (doNotFragment) {
			fragmentData = fragmentData | SET_DO_NOT_FRAGMENT;
		}

		// Bitwise operation to set the appropriate bit.
		if (moreBits) {
			fragmentData = fragmentData | SET_MORE_BITS;
		}

		buffer.putShort((short)fragmentData);

		// Append time to live (Number of hops left), 8 bits.
		buffer.put((byte)(timeToLive - BYTE_OFFSET));

		// Append protocol, 8 bits. Value = 6, corresponding to TCP protocol.
		buffer.put((byte)(6 - BYTE_OFFSET));

		/* Bytes 10 and 11 are reserved for the checksum value of the
		 * IPv4 datagram. Bytes are being added via 'relative put',
		 * so just use a filler value to skip them for now.
		 */
		buffer.putShort((short)555);   // Filler value for bytes 10 and 11.

		// Append source IP, 32 bits.
		buffer.put(sourceIp.toByteArray());

		// Append destination IP, 32 bits.
		buffer.put(destinationIp.toByteArray());

		// No options or padding according to project specifications.

		// Append data, variable bit length.
		buffer.put(payload);

		/* Append total length, 16 bits.
		 * Calculate the total length of the IPv4 datagram by using the relative
		 * position of the buffer and insert the value into bytes 3 and 4. 
		 */
		buffer.putShort(2, (short)(buffer.position()));

		// TODO: Implement the checksum method(s).
		/* Append checksum, 16 bits.
		 * Calculate the checksum of the IPv4 datagram and insert the value
		 * into bytes 10 and 11. 
		 */
		buffer.putShort(10, calculateChecksum(buffer));

		// Extract the datagram array from the buffer.
		int size = buffer.position();

		byte[] datagramArray = new byte[size];

		for (int i = 0; i < size; i++) {
			datagramArray[i] = buffer.get(i);
		}

		return datagramArray;
	}

	/**
	 * Returns a string representation of the bytes in an IPv4 datagram.
	 * 
	 * @return Returns a string representation of an IPv4 datagram.
	 */
	@Override
	public String toString() {
		StringBuilder dataString = new StringBuilder();

		for (int i = 0; i < datagram.length; i++) {
			dataString.append(datagram[i]);
			dataString.append(" ");
		}

		dataString.trimToSize();

		return dataString.substring(0);
	}

	/**
	 * Calculates the checksum value of an IPv4 Datagram.
	 * 
	 * @return
	 */
	private short calculateChecksum(ByteBuffer buffer) {
		/* To calculate the checksum, use the procedure taken from:
		 * http://en.wikipedia.org/wiki/IPv4_header_checksum
		 *
		 * Step 1, calculate the sum of each 16-bit value of the header.
		 */
		int sum = 0;

		/* Bytes 10 and 11 of the IP datagram contain the checksum value and
		 * must be omitted from the checksum calculation.
		 * Calculate the sum of bytes 0 - 9.
		 */
		for (int i = 0; i < 10; i++) {
			sum += buffer.get(i);
		}

		// Skip bytes 10 and 11 and continue calculating the sum.
		for (int i = 12; i < buffer.position(); i++) {
			sum += buffer.get(i);
		}

		/* Step 2, convert the sum to binary and add the carry to the rest of
		 * the sum values.
		 * 
		 * Use strings to manipulate binary values.
		 */	
		String sumString = Integer.toBinaryString(sum);

		/* The first 4 bits are the carry and will be added to the rest of
		 * the value. To compensate for the loss of leading zeros, reverse
		 * the string and extract substrings from the left side.
		 */
		sumString = reverseString(sumString);

		/* Extract the carry values and reverse the resulting substring to
		 * get its original value.
		 */
		int carry = 0;

		if (sumString.length() > 15) {
			String s1 = sumString.substring(16);
			s1 = reverseString(s1);
			carry = Integer.valueOf(s1, 2);
		}

		/* Extract the rest of the sum and reverse the resulting substring
		 * to get its original value.
		 */
		int restOfSum = 0;

		if (sumString.length() > 15) {
			String s2 = sumString.substring(0, 15);
			s2 = reverseString(s2);   	
			restOfSum = Integer.valueOf(s2, 2);
		} else {
			sumString = reverseString(sumString);
			restOfSum = Integer.valueOf(sumString, 2);
		}

		// TODO: Remove this print.
		System.out.println("[first] " + carry + "  [second] " + restOfSum);

		sum = carry + restOfSum;

		// Step 3, flip every bit to obtain the checksum.
		sum = ~sum;

		// Return a 16-bit value.
		return (short)sum;
	}

	/**
	 * Reverses the characters in a string.
	 * 
	 * @param s
	 * @return
	 */
	private String reverseString(String s) {
		StringBuffer sb = new StringBuffer(s).reverse();

		return sb.toString();	
	}
	
	private boolean validateChecksum(short value) {
		int sum = 0;

		for (int i = 0; i < datagram.length; i++) {
			sum += datagram[i];
		}
		
		String sumString = Integer.toBinaryString(sum);
		
		int carry = 0;

		if (sumString.length() > 15) {
			String s1 = sumString.substring(16);
			s1 = reverseString(s1);
			carry = Integer.valueOf(s1, 2);
		}
		
		int restOfSum = 0;

		if (sumString.length() > 15) {
			String s2 = sumString.substring(0, 16);
			s2 = reverseString(s2);   	
			restOfSum = Integer.valueOf(s2, 2);
		} else {
			sumString = reverseString(sumString);
			restOfSum = Integer.valueOf(sumString, 2);
		}
		
		sum = carry + restOfSum;
		
		sum = (short)~sum;
		
		if (sum == value) {
			return true;
		}
		
		return false;
	}
	
	public boolean validateChecksum(IPv4Datagram one, IPv4Datagram two) {
	
		
		return false;
	}
	
	public byte[] getDestinationIp () {		
		
		return Arrays.copyOfRange(datagram, 16, 20);
	}
}
